/*****************************************************************************
 * Copyright (C) Stacy Curl. All rights reserved.                            *
 * ------------------------------------------------------------------------- *
 * The software in this package is published under the terms of the BSD      *
 * style license a copy of which has been included with this distribution in *
 * the LICENSE.txt file.                                                     *
 *****************************************************************************/
package picounit.features.generic_return_type;

import picounit.features.base.Caller;
import picounit.features.classloader.observer.DefaultClassObserver;
import picounit.features.mocking.RecordingPlaybackMock;
import picounit.features.mocking.ReturnTypeModifier;

class CheckCastAwareReturnTypeModifier extends DefaultClassObserver
	implements ReturnTypeModifier {

	private static final int DEPTH_TO_CALLER = 12;
	private static final int DEPTH_TO_CALLER_VIA_CONVERT_INVOCATION_HANDLER =
		DEPTH_TO_CALLER + 1;

	private final CheckCastRegistry checkCastRegistry;

	public CheckCastAwareReturnTypeModifier(CheckCastRegistry checkCastRegistry) {
		super(new CheckCastClassVisitor(checkCastRegistry));

		this.checkCastRegistry = checkCastRegistry;
	}

	///////////////////////////////////////////////////////////////////////////////////////////////
	// ReturnTypeModifier methods
	///////////////////////////////////////////////////////////////////////////////////////////////

	@SuppressWarnings("unchecked")
	public <E> Class<E> refine(Class<E> returnType, Class<?> declaringClass)
		throws ClassNotFoundException {

		return getCastForCaller(getCaller(declaringClass))
			.refine(returnType, getClassLoader());
	}

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Implementation
	///////////////////////////////////////////////////////////////////////////////////////////////

	private Cast getCastForCaller(Caller caller) {
		return checkCastRegistry.getCast(caller);
	}

	private Caller getCaller(Class declaringClass) {
		StackTraceElement[] stackTrace = new Throwable().getStackTrace();

		boolean foundRecordingPlaybackMock = false;
		boolean foundProxy = false;
		
		for (StackTraceElement stackTraceElement : stackTrace) {
			String className = stackTraceElement.getClassName();

			if (foundRecordingPlaybackMock && foundProxy) {
				if (className.indexOf("$Proxy") == -1) {
					return new Caller(stackTraceElement);
				}
			}

			// TODO: Cyclic dependancy due to reference to RecordingPlaybackMock 
			if (className.equals(RecordingPlaybackMock.class.getName()) && 
				stackTraceElement.getMethodName().equals("invoke")) {

				foundRecordingPlaybackMock = true;
			}

			if (foundRecordingPlaybackMock) {
				if (className.indexOf("$Proxy") != -1 ||
					className.indexOf("$$EnhancerByCGLIB$$") != -1) {
					foundProxy = true;
				}
			}
		}
		
		int depthToCaller = depthToCaller(declaringClass);

		return new Caller(stackTrace[depthToCaller]);
	}

	// I should replace this with code that does not depend on a fixed calling depth
	private int depthToCaller(Class declaringClass) {
		return declaringClass.isInterface() ? DEPTH_TO_CALLER
			: DEPTH_TO_CALLER_VIA_CONVERT_INVOCATION_HANDLER;
	}
}